"""
Copyright 2009, Thomas Dejanovic, Jay Shurtz.
 
This is free software; you can redistribute it and/or modify it
under the terms of the GNU Lesser General Public License as
published by the Free Software Foundation; either version 2.1 of
the License, or (at your option) any later version.
 
This software is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
 
You should have received a copy of the GNU Lesser General Public
License along with this software; if not, write to the Free
Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
02110-1301 USA, or see the FSF site: http://www.fsf.org.

This module contains the base class for the HatchNode class tree,
related types, & functions.
"""

#----------------------------------------------------------------------

id = "$Id: hatch_node.py 667 2010-07-01 00:12:17Z jayshurtz $"
# $URL: http://hatch.googlecode.com/svn/trunk/hatch/hatch_nodes/hatch_node.py $
# $Author: jayshurtz $
version = " ".join(id.split()[1:3])

#----------------------------------------------------------------------

import to_hatch_structure
from hatch_constants import NAME_ATTRIBUTE, PARENT_ATTRIBUTE, PROPERTIES_ATTRIBUTE
from hatch_constants import CHILDNODES_ATTRIBUTE, ILLEGAL_NAMES
from hatch_generic_methods import hatchNodeClassAndName, hatchNodeErrorString

def all_base_classes(objClass):
    """ Get all base class for an object class recursively.
        'objClass' is the object class (the '.__class__' attribute.)
        Returns a set of class types.
    """
    bases = set()
    for base in objClass.__bases__:
        bases |= all_base_classes(base)
    return set([objClass]) | bases


class NodeProperties(dict):
    """ A class for HatchNode properties.
        Behaviour is slightly different from an ordinary dictionary in that
        this object has an 'owner' (the HatchNode that has these properties)
        and certain 'keys' are not allowed.
    """
    def __init__(self, owner, init={}):
        """ 'owner' is the HatchNode that has these properties.
            'init' is an ordinary dictionary.
        """
        self.owner = owner
        dict.__init__(self, init)

    def __setitem__(self, k, v):
        """ Change the property indicated to the new value.
            Properties are not allowed to have names that are in the HatchNode dictionary,
            for example, HatchNode.name is a required value, so there can be no 'name' property.
        """
        if k in self.owner.__dict__: # HatchNode attributes cannot be properties, extra protection for .name, .properties, etc.
            raise TypeError, hatchNodeErrorString(self.owner, "Property " + str(k) + " is reserved for use by hatch.")
        if HatchNode in all_base_classes(v.__class__):  # Ensure that properties are not HatchNode instances.
            raise TypeError, hatchNodeErrorString(self.owner, "Property " + str(k) + " cannot be a HatchNode or child class instance.")
        else:
            dict.__setitem__(self, k, v)


class NodeChildren(list):   # TODO - inherit from set ? Shouldn't have same child node twice (currently parent check handles this.)
    """ A class for the HatchNode child nodes list.
        Behaviour is slightly different from an ordinary list in that
        each entry must be a HatchNode instance. The parent field will 
        be set automatically to the HatchNode that holds this list.

        A HatchNode cannot have the same child node added twice
        because the child's parent value will already be set.
        if multiple parents are enabled, then this class will need to
        check that a child is not being added to the list twice.
    """
    def __init__(self, owner, init=[]):
        """ 'owner' is the HatchNode that has these children.
            'init' is an ordinary list.
        """
        self.owner = owner
        for childNode in init:
            self.__setparent(childNode)
        list.__init__(self, init)

    def __setparent(self, childNode):
        """ Check & set childNode's .parent attribute.
        """
        childNode.check_parent(self.owner)  # Raise error if this is not okay, or childNode is not a HatchNode.
        object.__setattr__(childNode, PARENT_ATTRIBUTE, self.owner)   # Set parent, advoid HatchNode.__setattr__('parent').

    def __add__(self, childNode):
        self.__setparent(childNode)
        return NodeChildren(self.owner, list.__add__(self, childNode))  # Keeps owner as in +=.
    def __iadd__(self, childNode):
        self.__setparent(childNode)
        return list.__iadd__(self, childNode)

    def __setitem__(self, item, childNode):
        self.__setparent(childNode)
        return list.__setitem__(self, item, childNode)

    def append(self, childNode):
        self.__setparent(childNode)
        return list.append(self, childNode)
    def extend(self, childNode):
        self.__setparent(childNode)
        return list.extend(self, childNode)
    def insert(self, index, childNode):
        self.__setparent(childNode)
        return list.insert(self, index, childNode)


class HatchNode(object):
    """ Base class for hatchable objects. Defines a way of sticking things 
        together in a heirchy; a HatchNode can any number of children, but only 
        one parent (like a B-tree), as so:

                        node_0          (block node, has 1+ children)
                        /  |  \ 
                     n_1  n_2  n_3
                     / \         \ 
                  n_4   n_5       n_6   (leaf node, has no children)

        The "class attributes" of a hatch node ('.name', '.childNodes' & '.parent') 
        are used internally by hatch to map out how a design is structured. These values 
        always require special handling, and are usually not set by the user directly.

        The "properties" of a hatch node *instance* (which can be almost anything that 
        the user wishes to set) serve as a flexible alternative to what might otherwise
        be implemented as additional attributes in child classes using inhertance.
        A more generic approach that keeps the hatch nodes class tree small is to use 
        the property list instead. For example, the width in bits of a register could 
        be implemented as 'register.bit_width', but 'register.properties['bit_width']'
        is more flexible.
    """
    def __init__(self, name, *childNodes, **properties):
        """ Create a new HatchNode instance.
            The 'name' argument MUST be passed in (as it has no default), so using a **keyarg 
            like name="nombre" raises the TypeError "got multiple values for keyword argument 'name'."
            'properties' is used to hold 'optional properties' of the instance, which must be specified by name.
            'childNodes' is used to hold any child nodes that are part of this node. A Node has no child nodes is a leaf.
        """
        self.__setattr__(NAME_ATTRIBUTE, name)
        object.__setattr__(self, PARENT_ATTRIBUTE, None)    # The HatchNode that has this object as a child.
        object.__setattr__(self, PROPERTIES_ATTRIBUTE, NodeProperties(self))
        object.__setattr__(self, CHILDNODES_ATTRIBUTE, NodeChildren(self))
        for (k, v) in properties.items():
            self.properties[k] = v  # Automatically checks keys.
        for c in childNodes:
            self.childNodes.append(c)   # Automatically adds self as parent.

    def visit(self, obj):   # XXX Use a callback instead of an object ?
        """ Allow self to be visited by different objects.
            'obj' is the visitor object, such as a design-sanity checker.
            Returns nothing, visitor objects should merely check/set hatch node properties.
        """
        obj.visit(self)

    def __str__(self):
        """ Return name of object.
            Note: __repr__() should NOT be overridden.
        """
        return self.name

    def copy(self, name=None, parent=None):
        """ Copy this node, it's properties, and recursively copy child nodes.
            Nodes that have been copied already are not tracked with a memo 
            dictionary as HatchNodes may not be self referential.  If this is 
            changed, copied objects should be tracked as in the copy.deepcopy() 
            method.  Shallow copies are not allowed in hatch (this would 
            effectively give a node more than one name/parent.)
            'name' is the name of the new node, defaults to original node's name.
            'parent' is the parent node of the new node, defaults to None.
            Returns a copy of this node.
        """
        #def __deepcopy__(self, memo):
        #     x = self.__class__.__new__(self.__class__)
        #     memo[id(self)] = x
        #     dict = {}
        #     for n, v in self.__dict__.iteritems():
        #         dict[n] = copy.deepcopy(v, memo)
        #     x.__setstate__(dict)
        #     return x
        #NodeProperties(owner=self.owner, init=self)
        if name is None:
            name = self.name
        newNode = self.__class__.__new__(self.__class__)    # Copy this 
        newNode.__init__(name)
        for (k, v) in self.properties.items():
            newNode.properties[k] = v
        for c in self.childNodes:
            newNode.childNodes.append(c.copy(c.name)) # Recurse, making copies. Parent is set by appending to child nodes.
        newNode.parent = parent
        return newNode

    def info(self, recurse=True, indent=" "*4, startIndent=" "*4, *args, **keyargs):
        """ Return a string with information about this object.
            'recurse' flags for traversal through child nodes.
            'indent' is the recursive indent.
            'startIndent' is the starting indentation level.
            '*args & **keyargs' are passed to the class_info() function, which is 
            called here almost like a fixed callback.
            Returns the information string.
        """
        r = startIndent + hatchNodeClassAndName(self) + "\n"    # + " " + repr(self) + 
        r += startIndent + indent + "parent: " + str(self.parent) + "\n"
        if len(self.properties) > 0:
            r += startIndent + indent
            r += ("\n" + startIndent + indent).join([repr(k) + ": " + repr(v) for (k, v) in sorted(self.property_list())]) + "\n"
        r += self.class_info(*args, **keyargs)
        for c in self.child_nodes():
            if recurse:
                r += c.info(recurse, indent, startIndent + indent + indent) # Print content info, including contents.
            else:
                r += startIndent + indent + indent + c.__class__.__name__ + " - " + str(c) + "\n"  # Print name of content only
        return r

    def class_info(self, *args, **keyargs): # XXX Use a variable callback in .info(args) instead ?
        """ Additional information about this class to be printed by a .info() call before recursing into child nodes.
            Child classes should override this method to print any extra info.
        """
        return ""

    def check_parent(self, val):
        """ Check that a parent node is valid, usually before setting this objects parent attribute.
            'val' is the potential parent node.
            A hatch node can only have zero or one parent nodes.
            As such, parent may be set to None at any time, or set to another hatch node if an only if it is currently None.
            Circular references are not allowed, so if 'you' are a child you cannot also be 'my' parent, and vice-versa.
            Raises AttributeError if the parent value is not valid.
        """
        try:
            if val.has_parent(self):    # or self.has_child(val): # Don't need both.
                raise AttributeError, hatchNodeErrorString(self, "Cannot set parent to a value in own childNodes, this would be circular.")
        except AttributeError, e:   # val is probably None.
            pass
        if self.parent is not None and val is not None: # Only 0-1 parents: Allow set parent IF None, or set parent TO None.
            raise AttributeError, repr(self.name) + " parent value already set to " + str(self.parent)

    #def __getattr__(self, attr):
    def __delattr__(self, attr):
        """ """
        raise AttributeError, hatchNodeClassAndName(self) + ": Class attributes cannot be deleted."

    def __setattr__(self, attr, val):   # Use property() builtin instead?
        """ Encapsulate this class:
            Ensure that the parent value is valid.
            Ensure that properties and childNodes are not set directly.
        """
        if attr == NAME_ATTRIBUTE:
            object.__setattr__(self, attr, val) # Set name.
            if val in ILLEGAL_NAMES:
                raise AttributeError, hatchNodeErrorString(self, "Invalid value for '" + NAME_ATTRIBUTE + "' attribute.")
        elif attr == PARENT_ATTRIBUTE:
            self.check_parent(val)  # Raise if issue.
            if val is None:
                if self.parent is not None:
                    self.parent.childNodes.remove(self) # Remove self from parent.childNodes
            else:
                list.append(val.childNodes, self)   # Add self to childNodes, advoid infinite recursion.
            object.__setattr__(self, attr, val) # Set parent.
        elif attr == PROPERTIES_ATTRIBUTE:
            raise AttributeError, hatchNodeErrorString(self, "Class properties cannot be set directly.")
        elif attr == CHILDNODES_ATTRIBUTE:
            raise AttributeError, hatchNodeErrorString(self, "Class childNodes cannot be set directly.")
        else:
            raise AttributeError, hatchNodeErrorString(self, "Class attribute " + repr(attr) + " cannot be set.")

    def has_property(self, p):  # XXX A better alternative is to use "if hatchNode.properties.get('key', None) is not None:"
        """ Return True if the property is defined.
        """
        return p in self.properties

    def property_list(self):
        """ Return properties as list of keys, values.
        """
        return self.properties.items()

    # XXX move these to NodeChildren class ?
    def has_parent(self, p):
        """ Return True if any parent in the tree above this is the passed in HatchNode.
        """
        retVal = False
        if self.parent is not None and not retVal:
            retVal = self.parent == p or self.parent.has_parent(p)
        return retVal

    if False:   # Unused alternative to has_parent.
        def has_child(self, c):
            """ Return True if any child in the tree below this is the passed in HatchNode.
            """
            retVal = False
            for childNode in self.childNodes and not retVal:
                retVal |= childNode == c or childNode.has_child(c) 
            return retVal

    # XXX Legacy functions - these behaviors are now part of the objects themselves
    # and should probably not be used in future code.
    # However they DO encapsulate the implementation, so it doesn't hurt to keep them.
    if True:
        def set_property(self, k, v):
            """ Change the property indicated to the new value.
            """
            #if k in self.__dict__: # Extra protection for .name, .properties, etc.
            #    raise TypeError, "Invalid keyword argument %s" % k
            #else:
            #    self.properties[k] = v
            self.properties[k] = v

        def property(self, p):  # Rename as get_property
            """ Return the property requested if it is defined, else return None.
            """
            #if self.has_property(p):
            #    return self.properties[p]
            return self.properties[p]

        def del_property(self, p):
            """ Delete the property indicated.
            """
            #if p in self.properties:
            #    del self.properties[p]
            #else:
            #    raise AssertionError, "Cannot delete %s." % p  # Use KeyError ?
            del self.properties[p]

        def contents(self):
            """ Return the internal contents list.
            """
            return self.child_nodes()

        def child_nodes(self):
            """ """
            return self.childNodes

        def add_content(self, node):
            """ Append this thing to the internal content list.
            """
            self.add_child_node(node)

        def add_child_node(self, node):
            """ """
            self.childNodes.append(node)
            #node.parent = self  # NodeChildren class automatically set this.

    # XXX Legacy traversals, no longer used as visitor objects can traverse a design themselves.
    if False:
        def parent_has_property(self, p):
            """ Return True if any parent in the tree above this has the
                property defined.
            """
            retVal = False
            if self.parent is not None:
                retVal = self.parent.has_property(p) or self.parent.parent_has_property(p)  # Clever recursion.
            return retVal

        def parent_property(self, p):
            """ Return None, or the property if any parent in the tree above this
                has the property defined. The property is returned by the 
                closest parent only.
            """
            if self.parent is not None:
                if self.parent.has_property(p):
                    return self.parent.property(p)
                else:
                    return self.parent.parent_property(p)

        def parent_property_list(self, p):
            """ Return a list of values for this property from *all* parents in the tree above this.
            """
            r = []
            if self.parent is not None:
                if self.parent.has_property(p):
                    r.append(self.parent.property(p))
                r += self.parent.parent_property_list(p)
            return r

        def run_on_all(self, function, **keyArgs):
            """ Run the given function on all elements in the hatch data
                structure, doing a depth first recursion.
            """
            for c in self.childNodes:
                c.run_on_all(function, **keyArgs)
            function(self, **keyArgs)   # Run on self last.

        def run_on_all_return_list(self, function, **keyArgs):
            """ Run the given function on all elements in the hatch data
                structure, doing a depth first recursion and build a list
                result.
            """
            r = []
            for c in self.childNodes:
                r += c.run_on_all_return_list(function, **keyArgs)
            r += function(self, **keyArgs)
            return r

        def run_on_all_return_or(self, function, **keyArgs):
            """ Run the given function on all elements in the hatch data
                structure, doing a depth first recursion returning a reduction
                OR on the boolean result.
            """
            r = function(self, **keyArgs)   # Run on self first.
            if not r:   # Return the instant we get a True result.
                for c in self.childNodes:
                    if c.run_on_all_return_or(function, **keyArgs):
                        r = True
                        break
            return r


# Test that the hatch node constants are valid.
# This also implicitly tests that the items in HATCH_NODE_ATTRIBUTES 
# cannot be hatch node properties, see NodeProperties.__setitem__().

from hatch_constants import HATCH_NODE_ATTRIBUTES

hatchNode = HatchNode("test")
if set(hatchNode.__dict__.keys()) != HATCH_NODE_ATTRIBUTES:
    notMatch = "The hatch node attributes defined in hatch_constants.py do"
    notMatch += " not match the HatchNode class attributes in hatch_node.py."
    raise AssertionError, notMatch
del hatchNode, HATCH_NODE_ATTRIBUTES


# Using the HatchLeaf class allows for stricter typing, but this is optional.
# Adding checks to the visitors methods is the preferred alternative, as this
# is how the physical classes do all their other sanity checking.  Otherwise
# HatchNodes that strictly have zero or non-zero children require extra classes.
if False: 
    class LeafChildren(NodeChildren):
        """ LeafChildren are used by the HatchLeaf class,
            and NodeChildren are used by the HatchNode class.
        """
        def __init__(self, owner, init=[]):
            NodeChildren.__init__(self, owner, init)
            self.errString = "Objects of type '" + self.owner.__class__.__name__ + "' cannot have any child nodes."
        def __add__(self, childNode):
            raise AttributeError, self.errString
        def __iadd__(self, childNode):
            raise AttributeError, self.errString
        def __setitem__(self, item, childNode):
            raise AttributeError, self.errString
        def __setslice__(self, i, j, childNode):
            raise AttributeError, self.errString
        def append(self, childNode):
            raise AttributeError, self.errString
        def extend(self, childNode):
            raise AttributeError, self.errString
        def insert(self, index, childNode):
            raise AttributeError, self.errString

    class HatchLeaf(HatchNode):
        """ A HatchLeaf is a specific type of HatchNode with no child nodes.
            It still supports the entire HatchNode interface to make recursive calls easy.
        """
        def __init__(self, name, **properties):
            """ """
            HatchNode.__init__(self, name, **properties)
            self.__dict__['childNodes'] = LeafChildren(self)


if __name__ == "__main__":  # TODO - Run this script for BIST, check against constants automatically when done.

    h = HatchNode('h', val='hval')   # Test properties
    g = HatchNode('g', h, val='gval')  # Test childNodes
    f = HatchNode('f', val='fval', prop='fprop')
    f.childNodes.append(g)
    d = HatchNode('d', val='dval')
    d.parent = f

    items = [h, g, f, d]

    if False:
        for i in items:

            print i
        #if str(h) == ""
            print str(i) + ".info: "
            print i.info()
            print '~~~~~~~~~~'
            print i.info(False)
            print '~~~~~~~~~~'

        for i in items:
            print str(i) + ".property_list: "
            print i.property_list()
            print '~~~~~~~~~~'
            print str(i) + ".contents: "
            print i.contents()
            print '~~~~~~~~~~'
            print str(i) + ".child_nodes: "
            print i.child_nodes()
            print '~~~~~~~~~~'

        for i in items:
            i.set_property('ex', 'extra')
            print str(i) + ".has_property(val,prop,ex): "
            print i.has_property('val'), i.has_property('prop'), i.has_property('ex')
            print '~~~~~~~~~~'
            print str(i) + ".property(val,prop,ex): "
            print i.property('val'), i.property('prop'), i.has_property('ex')
            print '~~~~~~~~~~'

        for i in items:
            i.del_property('ex')
            print '~~~~~~~~~~'
            print str(i) + ".has_property(val,prop,ex): "
            print i.has_property('val'), i.has_property('prop'), i.has_property('ex')
            print '~~~~~~~~~~'
            print str(i) + ".property(val,prop,ex): "
            print i.property('val'), i.property('prop'), i.has_property('ex')
            print '~~~~~~~~~~'

        if False:
            f.add_child_node(d) # XXX Test error when trying to add to multiple parents.
            print str(f) + ".info: "
            print f.info()
            print '~~~~~~~~~~'

            # Traversals up
            print g.parent_has_property('prop'), h.parent_has_property('prop')
            print '~~~~~~~~~~'
            print g.parent_property('prop'), h.parent_property('prop')
            print '~~~~~~~~~~'
            print g.parent_property_list('val'), h.parent_property_list('val')
            print '~~~~~~~~~~'

            for i in items:
                def printName(node):
                    print str(node)
                def getName(node):
                    return str(node)
                i.run_on_all(printName)
                print '~~~~~~~~~~'
                print i.run_on_all_return_list(getName)
                print '~~~~~~~~~~'
                print i.run_on_all_return_or(getName)
                print '~~~~~~~~~~'

    if False:
        print h.has_parent(f)
        print h.has_parent(g)
        print h.has_parent(d)

        try:
            d.properties['bb'] = f
        except Exception, e:
            print e

    if False:
        print f.has_child(h)
        print g.has_child(h)
        print d.has_child(h)

    if False:
        try:
            d.childNodes += g
        except Exception, e:
            print e
        try:
            h.childNodes += f
        except Exception, e:
            print e
        try:
            f.elaborate()
        except Exception, e:
            print e
        try:
            l.childNodes.append(h)
        except Exception, e:
            print e

    if False:
        h.parent = None
        g.parent = None
        f.parent = None
        h.parent = g
        f.childNodes.append(g)
        print "g.parent: " + str(g.parent)
        print "g.childNodes: " + str(g.childNodes)
        h.parent = None
        h.parent = f
        print "g.childNodes: " + str(g.childNodes)
        print "f.childNodes: " + str(f.childNodes)

    if True:    # Test arrays
        hatchNodeArray = [HatchNode('element_'+str(i), HatchNode('child_'+str(i)), val='v') for i in range(0, 10)]
        for i in hatchNodeArray:
            print i.info()

    if True:    # Test copy
        s = f.copy('s', None)
        f.properties['copied_property'] = 'whoo!'
        a = HatchNode('a', val='aval')
        f.childNodes.append(a)
        print f.info()
        print s.info()
        raw_input()


